/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import path from 'path';
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {addTrailingSlash} from '@docusaurus/utils-common';
import {createDocsByIdIndex, toCategoryIndexMatcherParam} from '../docs';
import type {
  SidebarItemDoc,
  SidebarItemsGenerator,
  SidebarItemsGeneratorDoc,
  NormalizedSidebarItemCategory,
  NormalizedSidebarItem,
  SidebarItemCategoryLinkConfig,
} from './types';

const BreadcrumbSeparator = '/';

// Just an alias to the make code more explicit
function getLocalDocId(docId: string): string {
  return _.last(docId.split('/'))!;
}

export const CategoryMetadataFilenameBase = '_category_';
export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';

type WithPosition<T> = T & {
  position?: number;
  /** The source is the file/folder name */
  source?: string;
};

/**
 * A representation of the fs structure. For each object entry:
 * If it's a folder, the key is the directory name, and value is the directory
 * content; If it's a doc file, the key is the doc's source file name, and value
 * is the doc ID
 */
type Dir = {
  [item: string]: Dir | string;
};

// Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = ({
  numberPrefixParser,
  isCategoryIndex,
  docs: allDocs,
  item: {dirName: autogenDir},
  categoriesMetadata,
}) => {
  const docsById = createDocsByIdIndex(allDocs);
  const findDoc = (docId: string): SidebarItemsGeneratorDoc | undefined =>
    docsById[docId];
  const getDoc = (docId: string): SidebarItemsGeneratorDoc => {
    const doc = findDoc(docId);
    if (!doc) {
      throw new Error(
        `Can't find any doc with ID ${docId}.
Available doc IDs:
- ${Object.keys(docsById).join('\n- ')}`,
      );
    }
    return doc;
  };

  /**
   * Step 1. Extract the docs that are in the autogen dir.
   */
  function getAutogenDocs(): SidebarItemsGeneratorDoc[] {
    function isInAutogeneratedDir(doc: SidebarItemsGeneratorDoc) {
      return (
        // Doc at the root of the autogenerated sidebar dir
        doc.sourceDirName === autogenDir ||
        // Autogen dir is . and doc is in subfolder
        autogenDir === '.' ||
        // Autogen dir is not . and doc is in subfolder
        // "api/myDoc" startsWith "api/" (note "api2/myDoc" is not included)
        doc.sourceDirName.startsWith(addTrailingSlash(autogenDir))
      );
    }
    const docs = allDocs.filter(isInAutogeneratedDir);

    if (docs.length === 0) {
      logger.warn`No docs found in path=${autogenDir}: can't auto-generate a sidebar.`;
    }
    return docs;
  }

  /**
   * Step 2. Turn the linear file list into a tree structure.
   */
  function treeify(docs: SidebarItemsGeneratorDoc[]): Dir {
    // Get the category breadcrumb of a doc (relative to the dir of the
    // autogenerated sidebar item)
    // autogenDir=a/b and docDir=a/b/c/d => returns [c, d]
    // autogenDir=a/b and docDir=a/b => returns []
    // TODO: try to use path.relative()
    function getRelativeBreadcrumb(doc: SidebarItemsGeneratorDoc): string[] {
      return autogenDir === doc.sourceDirName
        ? []
        : doc.sourceDirName
            .replace(addTrailingSlash(autogenDir), '')
            .split(BreadcrumbSeparator);
    }
    const treeRoot: Dir = {};
    docs.forEach((doc) => {
      const breadcrumb = getRelativeBreadcrumb(doc);
      // We walk down the file's path to generate the fs structure
      let currentDir = treeRoot;
      breadcrumb.forEach((dir) => {
        if (typeof currentDir[dir] === 'undefined') {
          currentDir[dir] = {}; // Create new folder.
        }
        currentDir = currentDir[dir] as Dir; // Go into the subdirectory.
      });
      // We've walked through the path. Register the file in this directory.
      currentDir[path.basename(doc.source)] = doc.id;
    });
    return treeRoot;
  }

  /**
   * Step 3. Recursively transform the tree-like structure to sidebar items.
   * (From a record to an array of items, akin to normalizing shorthand)
   */
  function generateSidebar(
    fsModel: Dir,
  ): WithPosition<NormalizedSidebarItem>[] {
    function createDocItem(
      id: string,
      fullPath: string,
      fileName: string,
    ): WithPosition<SidebarItemDoc> {
      const {
        sidebarPosition: position,
        frontMatter: {
          sidebar_key: key,
          sidebar_label: label,
          sidebar_class_name: className,
          sidebar_custom_props: customProps,
        },
      } = getDoc(id);
      return {
        type: 'doc',
        id,
        position,
        source: fileName,
        // We don't want these fields to magically appear in the generated
        // sidebar
        ...(key !== undefined && {key}),
        ...(label !== undefined && {label}),
        ...(className !== undefined && {className}),
        ...(customProps !== undefined && {customProps}),
      };
    }
    function createCategoryItem(
      dir: Dir,
      fullPath: string,
      folderName: string,
    ): WithPosition<NormalizedSidebarItemCategory> {
      const categoryMetadata =
        categoriesMetadata[path.posix.join(autogenDir, fullPath)];
      const allItems = Object.entries(dir).map(([key, content]) =>
        dirToItem(content, key, `${fullPath}/${key}`),
      );

      // Try to match a doc inside the category folder,
      // using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
      function findDocByLocalId(localId: string): SidebarItemDoc | undefined {
        return allItems.find(
          (item): item is SidebarItemDoc =>
            item.type === 'doc' && getLocalDocId(item.id) === localId,
        );
      }

      function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
        return allItems.find((item): item is SidebarItemDoc => {
          if (item.type !== 'doc') {
            return false;
          }
          const doc = getDoc(item.id);
          return isCategoryIndex(toCategoryIndexMatcherParam(doc));
        });
      }

      // In addition to the ID, this function also retrieves metadata of the
      // linked doc that could be used as fallback values for category metadata
      function getCategoryLinkedDocMetadata():
        | {
            id: string;
            key?: string;
            position?: number;
            label?: string;
            customProps?: {[key: string]: unknown};
            className?: string;
          }
        | undefined {
        const link = categoryMetadata?.link;
        if (link !== undefined && link?.type !== 'doc') {
          // If a link is explicitly specified, we won't apply conventions
          return undefined;
        }
        const id = link
          ? findDocByLocalId(link.id)?.id ?? getDoc(link.id).id
          : findConventionalCategoryDocLink()?.id;
        if (!id) {
          return undefined;
        }
        const doc = getDoc(id);
        return {
          id,
          position: doc.sidebarPosition,
          key: doc.frontMatter.sidebar_key,
          label: doc.frontMatter.sidebar_label ?? doc.title,
          customProps: doc.frontMatter.sidebar_custom_props,
          className: doc.frontMatter.sidebar_class_name,
        };
      }
      const categoryLinkedDoc = getCategoryLinkedDocMetadata();
      const link: SidebarItemCategoryLinkConfig | null | undefined =
        categoryLinkedDoc
          ? {
              type: 'doc',
              id: categoryLinkedDoc.id, // We "remap" a potentially "local id" to a "qualified id"
            }
          : categoryMetadata?.link;
      // If a doc is linked, remove it from the category subItems
      const items = allItems.filter(
        (item) => !(item.type === 'doc' && item.id === categoryLinkedDoc?.id),
      );

      const className =
        categoryMetadata?.className ?? categoryLinkedDoc?.className;
      const customProps =
        categoryMetadata?.customProps ?? categoryLinkedDoc?.customProps;
      const {filename, numberPrefix} = numberPrefixParser(folderName);

      const key = categoryMetadata?.key ?? categoryLinkedDoc?.key;

      return {
        type: 'category',
        label: categoryMetadata?.label ?? categoryLinkedDoc?.label ?? filename,
        collapsible: categoryMetadata?.collapsible,
        collapsed: categoryMetadata?.collapsed,
        position:
          categoryMetadata?.position ??
          categoryLinkedDoc?.position ??
          numberPrefix,
        source: folderName,
        ...(customProps !== undefined && {customProps}),
        ...(className !== undefined && {className}),
        items,
        ...(categoryMetadata?.description && {
          description: categoryMetadata?.description,
        }),
        ...(key && {key}),
        ...(link && {link}),
      };
    }
    function dirToItem(
      dir: Dir | string, // The directory item to be transformed.
      itemKey: string, // File/folder name; for categories, it's used to generate the next `relativePath`.
      fullPath: string, // `dir`'s full path relative to the autogen dir.
    ): WithPosition<NormalizedSidebarItem> {
      return typeof dir === 'object'
        ? createCategoryItem(dir, fullPath, itemKey)
        : createDocItem(dir, fullPath, itemKey);
    }
    return Object.entries(fsModel).map(([key, content]) =>
      dirToItem(content, key, key),
    );
  }

  /**
   * Step 4. Recursively sort the categories/docs + remove the "position"
   * attribute from final output. Note: the "position" is only used to sort
   * "inside" a sidebar slice. It is not used to sort across multiple
   * consecutive sidebar slices (i.e. a whole category composed of multiple
   * autogenerated items)
   */
  function sortItems(
    sidebarItems: WithPosition<NormalizedSidebarItem>[],
  ): NormalizedSidebarItem[] {
    const processedSidebarItems = sidebarItems.map((item) => {
      if (item.type === 'category') {
        return {...item, items: sortItems(item.items)};
      }
      return item;
    });
    const sortedSidebarItems = _.sortBy(processedSidebarItems, [
      'position',
      'source',
    ]);
    return sortedSidebarItems.map(({position, source, ...item}) => item);
  }
  // TODO: the whole code is designed for pipeline operator
  const docs = getAutogenDocs();
  const fsModel = treeify(docs);
  const sidebarWithPosition = generateSidebar(fsModel);
  const sortedSidebar = sortItems(sidebarWithPosition);
  return sortedSidebar;
};
